More Flyweight with ECS
#udemy
#Unity
#Flyweight_Pattern
ECS = Entity Component System
Flyweight パターンの究極系とも言えるのが、
Unityの根本をなすEntity Component System
Monobehaviour と ScriptableObject
Unityのほぼすべてのクラスはこの2つを継承しており
設計思想もFlyweightパターンに準拠している。
どちらも、オブジェクトの共通部分を引き離して
1つのメモリ領域に格納し、それを参照させて使い回すという思想のクラス
ScriptableObject: データをゲームオブジェクトから引き離す
Monobehaviour: ゲーム内での挙動をゲームオブジェクトから引き離す
例えば、1000匹の魚が泳いでいるゲームシーンなど、
魚が泳ぐ制御を1インスタンスごとにコードがコピーされればとてつもないメモリ消費だが、
Monobehaviourとして分離して単一のインスタンスを参照すれば省メモリ
SetUp
.Net v4.x にする
1. 新しいプロジェクト
2. 上メニュー > File > Build Settings...
3. Player Settings...
4. Player > Other Settings > Api Compatibility Level
5. .Net 4.x にする (プラットフォーム共通設定)
Packages
1. Windows > Package Manager
2. Advanced > show preview package
開発版の不安定な機能を使える 
3. 使用するパッケージを選んで、ウィンドウ右下のInstallをクリック
Entities
Hybrid Renderer
Make Scene
以前のレッスンでやったように
SpawnerゲームオブジェクトのStart()に生成処理を書く。
width と depth を指定して、ランダムな高さをMathf.PerlinNoize()で作成した
CubePrefab をクローンした Terrain を作り、
自動車オブジェクトを配置、SmoothFollow.targetにcarを設定して
背後から追尾して運転できるゲームを作る
1000 * 1000 個のCube Terrain
この状態でシーンを再生するとすごくラグくなる。
講師のPCでは約273 MBのメモリ消費だった(Windows > Analysis > Profiler の Memory > mono )
これはゲームオブジェクトがすごく重いため
しかし、Entity Component System にすれば、 126 MBまで落とせる
(処理のメモリ領域共通化、並列処理による効果)
GameObject のコスト
ゲームオブジェクトで最もコストの高い部分は、Componentである
Crate > 3D > Cube
Cube(Mesh Filter)
MeshRenderer
BoxCollider
Material
ECS Coding
Entityとは
Unity が GameObjectを低コストに変換したもの
Hierarchy には表示されない
非常に重い Component 部分を共有する
なので、全体としては重くならない
Scriptable Objectと似たような機能
Cube を Entity に変換してみる
現在のバージョンのECSだと、少しコードを書く必要がある。
少し面倒なプロセスだが、パフォーマンスが劇的に上がる
講座では、
ECube(Prefab)
Swapner
の2つのゲームオブジェクトをそれぞれEntitiy化していた。
Cube の準備
1. Cubeをヒエラルキーに追加
2. Add Component > Script > Convert To Entity
Entities package で追加された項目
3. 共有するスクリプトなどを書く
PerlinPosition
PerlinPositionProxy
code: PerlinePositionProxy.cs
// 最初に書いてある using を消して以下に書き換える
using System;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
RequiresEntityConversion
public class PerlinPositionProxy : MonoBehaviour, IConvertGameObjectToEntity
{
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
// 空のコンポーネントを作る
var data = new PerlinPosition { };
dstManager.AddComponentData(entity, data);
}
}
code: PerlinPosition.cs
using System;
using Unity.Entities;
public class PerlinPosition : IComponentData
{
}
補完が効かないときは、
Packages/manifest.json の vscode 拡張を 1.1.4 > 1.1.3に下げてみる
.csproj と .slnファイルを削除して、Unity プロジェクトを再起動しつつ Asset > Open C# Project
4. PerlinPositionProxyの方を、Entityに変換するオブジェクトに追加
5. Prefab化する
Spawnerの準備
1. Create > Empty
2. Add Component > Convert To Entity
3. 以下のスクリプトを用意
Entity = Componentを管理する
Component = ある処理Systemを実行するのに必要なプロパティの集まり。メモリ上の連続した領域に入っている
code: SoawnerProxy.cs
using System;
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;
public class SpawnProxy : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
public GameObject cube;
public int rows;
public int cols;
public void DeclareReferencedPrefabs(List<GameObject> gameObjects)
{
gameObjects.Add(cube);
}
// Convert Method の引数は長いので、ECubeの方をコピペした
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var spawnerData = new Spawner
{
Prefab = conversionSystem.GetPrimaryEntity(cube),
Erows = rows,
Ecols = cols
};
dstManager.AddComponentData(entity, spawnerData);
}
}
code: Spawner.cs
using Unity.Entities;
public class Spawner : IComponentData
{
public Entity Prefab;
public int Erows;
public int Ecols;
}
4. System を書く
System = Entity Component System で、分離・共有化された処理の部分。pure function
code: SpawnerSystem.cs
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
public class SpawnerSystem : JobComponentSystem
{
EndSimulationEntityCommandBufferSystem m_EntityCommandBufferSystem;
protected override void OnCreateManager()
{
m_EntityCommandBufferSystem = World.GetOrCreateManager<EndSimulationEntityCommandBufferSystem>();
}
struct SpawnJob : IJobForEachWithEntity<Spawner, LocalToWorld>
{
public EntityCommandBuffer CommandBuffer;
public void Execute(Entity entity, int index, ReadOnly ref Spawner spawner, ReadOnly ref LocalToWorld location)
{
for (int x = 0; x < spawner.Erows; x++)
for (int z = 0; z < spawner.Ecols; z++)
{
var instance = CommandBuffer.Instantiate(spawner.Prefab);
var pos = math.transform(location.Value, new float3(x, noise.cnoise(new float2(x, z) * 0.21f), z));
CommandBuffer.SetComponent(instance, new Translation { Value = pos });
}
CommandBuffer.DestroyEntity(entity);
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new SpawnJob
{
CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer()
}.ScheduleSingle(this, inputDeps);
m_EntityCommandBufferSystem.AddJobHandleForProducer(job);
return job;
}
}
Vector3の代わりにmath.transform()関数や float3, float2クラスを使ったりと、
なるべくインスタンスを作らないようなコーディングになる
通常の Monobehaviour との実装の違い
あきた
というか、コードが古くて実行できない
現在 Entities = v0.1.0
講座 Entities = v0.0.14
Entityの操作
Entityの処理の実行はバックグラウンドで行われるため、実質的にStart()でしか制御できない
用語解説
UnityのECSを理解する その1(初心者向け) - EF Blog
Unity.Burst
高度に最適化されたマシンコードを生成するLLVMベースコンパイラであるBurst Compilerを使う機能
C# Job System
並列処理機能
Entity Component System (ECS)
データ指向アーキテクチャパターン
まとめ
ECS
ScriptableObject は ゲームオブジェクトからデータを分離する
ECS は、ゲームオブジェクトをEntityに変換して、処理を分離する
ECS の Componentは厳密には意味が違うらしい
Entity = GameObjectに近い。コンポーネントを管理するもの。処理を持たない。
Compoennt = ある処理を行うのに必要なプロパティだけを持つ、pure な struct。処理を持っていない
Archetype = Entity内のComponentの組み合わせ
Chunk = EntityとComponentを含むメモリ領域、必ず16KiB。コンポーネントをシュル別に並び替えたもの
キャッシュミスが少なくなり、処理速度が上がる。
System = 動作。処理を行うために必要な関数
データを持たない。つまりある入力に対し必ず同じ出力を返すpure function
Componentを持っているChunkを探し、計算を行い、Componentに書き込む
その他
Entity Manager = Entityを作成、読み取り、更新、破棄する
World = Entity Manager と ComponentSystemを包括する
World同士の鑑賞は不可能
つまり
ECSは並列化も適用した、Flyweight Design Patternテクニックを実装する完璧な例の一つだということ
現在はまだ開発中かつ抽象的だが、
コア部分はゲームオブジェクトの処理部分のメモリを1つだけ取って、それを参照させるという省リソース戦略